package net.homelinux.chaoswg.io.renga.core.gui.operations;

import java.awt.Graphics;
import java.awt.Point;
import java.util.List;
import java.util.Observable;
import java.util.Stack;
import java.util.Vector;

import net.homelinux.chaoswg.io.renga.core.gui.DrawingArea;
import net.homelinux.chaoswg.io.renga.core.gui.operations.helper.Connection;
import net.homelinux.chaoswg.io.renga.core.gui.operations.helper.RemovalListener;
import net.homelinux.chaoswg.io.renga.core.gui.operations.visualisations.Visualisation;

import org.jdom.Element;

public abstract class Operation extends Observable {
    protected static int nextID = 0;

    protected int id = nextID++;

    protected Visualisation visualisation = null;
    protected Connection [] connections = null;
    protected List<RemovalListener> removalListeners = new Vector<RemovalListener>();

    protected int inputs = 0;
    protected int outputs = 0;

    protected Object result  = null;
    
    protected boolean visible = false;

    public boolean isVisible() {
        return visible;
    }

    public void setVisible(boolean visible) {
        this.visible = visible;
    }

    public void drawYourself (final Graphics g) {
        visualisation.drawYourself(g);
        drawConnections(g);
    }

    public void drawConnections (final Graphics g) {
        for (int i = 0, n = this.getInputs(); i < n; ++i) {
            if (this.isConnected(i)) {
                final Point p0 = this.getInputConnectorCenter(i);

                Point p1 = null;
                /*Prevents the case that a connection exists but the operations are not visible to the
                 * Current view 
                 * This happens, for example, when we are inside a composite operation some FunctionInputOperations
                 * might be connected but the connectors they are connected to are in the surrounding 
                 * Operation hence nit visible here ;-)
                 * Another case are Variable Puts and Gets, which are connected but the connections aren't drawn
                 */
                if (this.getConnection(i).getInputOperation() != null) {
                    p1 = this.getConnection(i).getInputOperation().getOutputConnectorCenter(this.getConnection(i).getOutput());
                }
                if (p0 != null && p1 != null) {
                    g.drawLine(p0.x, p0.y, p1.x, p1.y);
                }
            }
        }

    }

    public void setSelected (final boolean selected) {
        visualisation.setSelected(selected);

        setChanged();
        notifyObservers(this);
    }

    public boolean isSelected () {
        return visualisation.isSelected();
    }

    public boolean connectAllowed (final Point p) {
        return visualisation.connectAllowed(p);
    }

    public final boolean contains (final Point p) {
        return visualisation.contains(p);
    }

    public Point getInputConnectorCenter (final int i) {
        return visualisation.getInputConnectorCenter(i);
    }

    public Point getOutputConnectorCenter (final int i) {
        return visualisation.getOutputConnectorCenter(i);
    }

    public void moveBy (final int dx, final int dy) {
        visualisation.moveBy(dx, dy);

        setChanged();
        notifyObservers(this);
    }

    public void moveTo (final Point position) {
        final int currentX = this.visualisation.getPosition().x;
        final int currentY = this.visualisation.getPosition().y;

        final int dx =  position.x - currentX;
        final int dy =  position.y - currentY;
        
        moveBy(dx, dy);
    }
    
    public void connect (final int i, final Operation otherEnd, final int output) {
        final Connection c = new Connection ();
        c.setInputOperation(otherEnd);
        c.setOutput(output);
        this.connections[i] = c;
    }

    public void connectTo (final Point thisPoint, final Operation other, final Point thatPoint) {
        final int thisInputConnectorIndex = visualisation.getInputConnectorIndex(thisPoint);
        final int thisOutputConnectorIndex = visualisation.getOutputConnectorIndex(thisPoint);
        final int thatInputConnectorIndex = other.visualisation.getInputConnectorIndex(thatPoint);
        final int thatOutputConnectorIndex = other.visualisation.getOutputConnectorIndex(thatPoint);

        if (thisInputConnectorIndex >= 0 && thatOutputConnectorIndex >= 0 && !this.producesCircle(other)) {
            this.connections[thisInputConnectorIndex].setInputOperation(other);
            this.connections[thisInputConnectorIndex].setOutput(thatOutputConnectorIndex);
        }

        if (thisOutputConnectorIndex >= 0 && thatInputConnectorIndex >= 0 && !other.producesCircle(this)) {
            other.connections[thatInputConnectorIndex].setInputOperation(this);
            other.connections[thatInputConnectorIndex].setOutput(thisOutputConnectorIndex);
        }
    }

    public boolean isConnected (final int i) {
        return connections[i].isConnected();
    }

    protected boolean producesCircle (final Operation in) {
        final Stack<Operation> superOperation = new Stack<Operation>();

        superOperation.push(in);

        while (!superOperation.isEmpty()) {
            final Operation current = superOperation.pop ();
            if (this == current) {
                return true;
            } else {
                if (current.connections != null) { 
                    for (final Connection c : current.connections) {
                        if (c.isConnected()) {
                            superOperation.push(c.getInputOperation());
                        }
                    }}
            }
        }
        return false;
    }

    public void reset () {
        visualisation.setError(false); 
        result = null;
    }

    public abstract Object apply (final int i);

    public String toString () {
        return "Generic Operation";
    }

    public Connection getConnection (final int i) {
        if (connections != null && i < connections.length) {
            return connections[i];
        }
        return null;
    }

    public int getInputs() {
        return inputs;
    }

    public void setInputs(final int inputs) {
        this.inputs = inputs;
    }

    public int getOutputs() {
        return outputs;
    }

    public void setOutputs(final int outputs) {
        this.outputs = outputs;
    }

    public Object getResult () {
        return result;
    }

    public void prepareForRemoval () {
        for (final RemovalListener rl : removalListeners) {
            rl.operationRemoved(this);
        }
    }

    public void addRemovalListener (final RemovalListener rl) {
        removalListeners.add(rl);
    }

    public void removeRemovalListener (final RemovalListener rl) {
        removalListeners.remove(rl);
    }

    public String getMoveActionName () {
        return "Connect";
    }

    public String getShiftMoveActionName () {
        return "Move";
    }

    public String getClickActionName () {
        return "Select";
    }

    public abstract String getDoubleClickActionName ();

    public abstract void doubleClick (final DrawingArea dv);

    public boolean fullyConnected () {
        if (connections == null) {
            return true;
        }
        for (final Connection c : connections) {
            if (! c.isConnected()) {
                return false;
            }
        }
        return true;
    }

    public int getMaxX () {
        return this.visualisation.getMaxX();
    }

    public int getMaxY () {
        return this.visualisation.getMaxY();
    }

    public int getId() {
        return id;
    }

    public void setId(final int id) {
        this.id = id;
    }
    
    public void initConnections (final int amount) {
        this.connections = new Connection[amount];
        
        for (int i =0; i < amount; ++i) {
            this.connections[i] = new Connection();
        }
    }

    public Element writeYourself () {
        final Element toReturn = new Element ("operation");
        toReturn.setAttribute("id", ""+ this.getId());
        toReturn.setAttribute("type" , this.getClass().getCanonicalName());

        final Element connectionsElement  = new Element ("connections");
        if (connections != null) {
            connectionsElement.setAttribute("total", "" + connections.length);
            for (int i = 0; i < connections.length; ++i) {
                Connection c = connections[i];
                final Operation otherEnd = c.getInputOperation();
                final int output = c.getOutput();
                if (otherEnd != null) {
                    final Element connectionElement = new Element ("connection");
                    connectionElement.setAttribute("input", i+"");
                    final Element otherEndID = new Element("id");
                    otherEndID.setText(otherEnd.getId() + "" );
                    Element otherEndOutput = new Element("outputNumber");
                    otherEndOutput.setText(""+output);

                    connectionElement.addContent(otherEndID);
                    connectionElement.addContent(otherEndOutput);
                    connectionsElement.addContent(connectionElement);
                }
            }
            toReturn.addContent(connectionsElement);
        }

        toReturn.addContent(visualisation.writeYourself ());

        return toReturn;
    }
    
    public static void setNextId (final int i) {
        nextID = i;
    }
}
